/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jasper.compiler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.jasper.JasperException;
import org.apache.jasper.JspCompilationContext;

/**
 * Contains static utilities for generating SMAP data based on the current
 * version of Jasper.
 * 
 * @author Jayson Falkner
 * @author Shawn Bayern
 * @author Robert Field (inner SDEInstaller class)
 * @author Mark Roth
 * @author Kin-man Chung
 */
public class SmapUtil {

	private org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory
			.getLog(SmapUtil.class);

	// *********************************************************************
	// Constants

	public static final String SMAP_ENCODING = "UTF-8";

	// *********************************************************************
	// Public entry points

	/**
	 * Generates an appropriate SMAP representing the current compilation
	 * context. (JSR-045.)
	 * 
	 * @param ctxt
	 *            Current compilation context
	 * @param pageNodes
	 *            The current JSP page
	 * @return a SMAP for the page
	 */
	public static String[] generateSmap(JspCompilationContext ctxt,
			Node.Nodes pageNodes) throws IOException {

		// Scan the nodes for presence of Jasper generated inner classes
		PreScanVisitor psVisitor = new PreScanVisitor();
		try {
			pageNodes.visit(psVisitor);
		} catch (JasperException ex) {
		}
		HashMap map = psVisitor.getMap();

		// set up our SMAP generator
		SmapGenerator g = new SmapGenerator();

		/**
		 * Disable reading of input SMAP because: 1. There is a bug here:
		 * getRealPath() is null if .jsp is in a jar Bugzilla 14660. 2. Mappings
		 * from other sources into .jsp files are not supported. TODO: fix 1. if
		 * 2. is not true. // determine if we have an input SMAP String smapPath =
		 * inputSmapPath(ctxt.getRealPath(ctxt.getJspFile())); File inputSmap =
		 * new File(smapPath); if (inputSmap.exists()) { byte[] embeddedSmap =
		 * null; byte[] subSmap = SDEInstaller.readWhole(inputSmap); String
		 * subSmapString = new String(subSmap, SMAP_ENCODING);
		 * g.addSmap(subSmapString, "JSP"); }
		 */

		// now, assemble info about our own stratum (JSP) using JspLineMap
		SmapStratum s = new SmapStratum("JSP");

		g.setOutputFileName(unqualify(ctxt.getServletJavaFileName()));

		// Map out Node.Nodes
		evaluateNodes(pageNodes, s, map, ctxt.getOptions().getMappedFile());
		s.optimizeLineSection();
		g.addStratum(s, true);

		if (ctxt.getOptions().isSmapDumped()) {
			File outSmap = new File(ctxt.getClassFileName() + ".smap");
			PrintWriter so = new PrintWriter(new OutputStreamWriter(
					new FileOutputStream(outSmap), SMAP_ENCODING));
			so.print(g.getString());
			so.close();
		}

		String classFileName = ctxt.getClassFileName();
		int innerClassCount = map.size();
		String[] smapInfo = new String[2 + innerClassCount * 2];
		smapInfo[0] = classFileName;
		smapInfo[1] = g.getString();

		int count = 2;
		Iterator iter = map.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry entry = (Map.Entry) iter.next();
			String innerClass = (String) entry.getKey();
			s = (SmapStratum) entry.getValue();
			s.optimizeLineSection();
			g = new SmapGenerator();
			g.setOutputFileName(unqualify(ctxt.getServletJavaFileName()));
			g.addStratum(s, true);

			String innerClassFileName = classFileName.substring(0,
					classFileName.indexOf(".class"))
					+ '$' + innerClass + ".class";
			if (ctxt.getOptions().isSmapDumped()) {
				File outSmap = new File(innerClassFileName + ".smap");
				PrintWriter so = new PrintWriter(new OutputStreamWriter(
						new FileOutputStream(outSmap), SMAP_ENCODING));
				so.print(g.getString());
				so.close();
			}
			smapInfo[count] = innerClassFileName;
			smapInfo[count + 1] = g.getString();
			count += 2;
		}

		return smapInfo;
	}

	public static void installSmap(String[] smap) throws IOException {
		if (smap == null) {
			return;
		}

		for (int i = 0; i < smap.length; i += 2) {
			File outServlet = new File(smap[i]);
			SDEInstaller.install(outServlet, smap[i + 1].getBytes());
		}
	}

	// *********************************************************************
	// Private utilities

	/**
	 * Returns an unqualified version of the given file path.
	 */
	private static String unqualify(String path) {
		path = path.replace('\\', '/');
		return path.substring(path.lastIndexOf('/') + 1);
	}

	/**
	 * Returns a file path corresponding to a potential SMAP input for the given
	 * compilation input (JSP file).
	 */
	private static String inputSmapPath(String path) {
		return path.substring(0, path.lastIndexOf('.') + 1) + "smap";
	}

	// *********************************************************************
	// Installation logic (from Robert Field, JSR-045 spec lead)
	private static class SDEInstaller {

		private org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory
				.getLog(SDEInstaller.class);

		static final String nameSDE = "SourceDebugExtension";

		byte[] orig;
		byte[] sdeAttr;
		byte[] gen;

		int origPos = 0;
		int genPos = 0;

		int sdeIndex;

		public static void main(String[] args) throws IOException {
			if (args.length == 2) {
				install(new File(args[0]), new File(args[1]));
			} else if (args.length == 3) {
				install(new File(args[0]), new File(args[1]), new File(args[2]));
			} else {
				System.err
						.println("Usage: <command> <input class file> "
								+ "<attribute file> <output class file name>\n"
								+ "<command> <input/output class file> <attribute file>");
			}
		}

		static void install(File inClassFile, File attrFile, File outClassFile)
				throws IOException {
			new SDEInstaller(inClassFile, attrFile, outClassFile);
		}

		static void install(File inOutClassFile, File attrFile)
				throws IOException {
			File tmpFile = new File(inOutClassFile.getPath() + "tmp");
			new SDEInstaller(inOutClassFile, attrFile, tmpFile);
			if (!inOutClassFile.delete()) {
				throw new IOException("inOutClassFile.delete() failed");
			}
			if (!tmpFile.renameTo(inOutClassFile)) {
				throw new IOException("tmpFile.renameTo(inOutClassFile) failed");
			}
		}

		static void install(File classFile, byte[] smap) throws IOException {
			File tmpFile = new File(classFile.getPath() + "tmp");
			new SDEInstaller(classFile, smap, tmpFile);
			if (!classFile.delete()) {
				throw new IOException("classFile.delete() failed");
			}
			if (!tmpFile.renameTo(classFile)) {
				throw new IOException("tmpFile.renameTo(classFile) failed");
			}
		}

		SDEInstaller(File inClassFile, byte[] sdeAttr, File outClassFile)
				throws IOException {
			if (!inClassFile.exists()) {
				throw new FileNotFoundException("no such file: " + inClassFile);
			}

			this.sdeAttr = sdeAttr;
			// get the bytes
			orig = readWhole(inClassFile);
			gen = new byte[orig.length + sdeAttr.length + 100];

			// do it
			addSDE();

			// write result
			FileOutputStream outStream = new FileOutputStream(outClassFile);
			outStream.write(gen, 0, genPos);
			outStream.close();
		}

		SDEInstaller(File inClassFile, File attrFile, File outClassFile)
				throws IOException {
			this(inClassFile, readWhole(attrFile), outClassFile);
		}

		static byte[] readWhole(File input) throws IOException {
			FileInputStream inStream = new FileInputStream(input);
			int len = (int) input.length();
			byte[] bytes = new byte[len];
			if (inStream.read(bytes, 0, len) != len) {
				throw new IOException("expected size: " + len);
			}
			inStream.close();
			return bytes;
		}

		void addSDE() throws UnsupportedEncodingException, IOException {
			int i;
			copy(4 + 2 + 2); // magic min/maj version
			int constantPoolCountPos = genPos;
			int constantPoolCount = readU2();
			if (log.isDebugEnabled())
				log.debug("constant pool count: " + constantPoolCount);
			writeU2(constantPoolCount);

			// copy old constant pool return index of SDE symbol, if found
			sdeIndex = copyConstantPool(constantPoolCount);
			if (sdeIndex < 0) {
				// if "SourceDebugExtension" symbol not there add it
				writeUtf8ForSDE();

				// increment the countantPoolCount
				sdeIndex = constantPoolCount;
				++constantPoolCount;
				randomAccessWriteU2(constantPoolCountPos, constantPoolCount);

				if (log.isDebugEnabled())
					log.debug("SourceDebugExtension not found, installed at: "
							+ sdeIndex);
			} else {
				if (log.isDebugEnabled())
					log.debug("SourceDebugExtension found at: " + sdeIndex);
			}
			copy(2 + 2 + 2); // access, this, super
			int interfaceCount = readU2();
			writeU2(interfaceCount);
			if (log.isDebugEnabled())
				log.debug("interfaceCount: " + interfaceCount);
			copy(interfaceCount * 2);
			copyMembers(); // fields
			copyMembers(); // methods
			int attrCountPos = genPos;
			int attrCount = readU2();
			writeU2(attrCount);
			if (log.isDebugEnabled())
				log.debug("class attrCount: " + attrCount);
			// copy the class attributes, return true if SDE attr found (not
			// copied)
			if (!copyAttrs(attrCount)) {
				// we will be adding SDE and it isn't already counted
				++attrCount;
				randomAccessWriteU2(attrCountPos, attrCount);
				if (log.isDebugEnabled())
					log.debug("class attrCount incremented");
			}
			writeAttrForSDE(sdeIndex);
		}

		void copyMembers() {
			int count = readU2();
			writeU2(count);
			if (log.isDebugEnabled())
				log.debug("members count: " + count);
			for (int i = 0; i < count; ++i) {
				copy(6); // access, name, descriptor
				int attrCount = readU2();
				writeU2(attrCount);
				if (log.isDebugEnabled())
					log.debug("member attr count: " + attrCount);
				copyAttrs(attrCount);
			}
		}

		boolean copyAttrs(int attrCount) {
			boolean sdeFound = false;
			for (int i = 0; i < attrCount; ++i) {
				int nameIndex = readU2();
				// don't write old SDE
				if (nameIndex == sdeIndex) {
					sdeFound = true;
					if (log.isDebugEnabled())
						log.debug("SDE attr found");
				} else {
					writeU2(nameIndex); // name
					int len = readU4();
					writeU4(len);
					copy(len);
					if (log.isDebugEnabled())
						log.debug("attr len: " + len);
				}
			}
			return sdeFound;
		}

		void writeAttrForSDE(int index) {
			writeU2(index);
			writeU4(sdeAttr.length);
			for (int i = 0; i < sdeAttr.length; ++i) {
				writeU1(sdeAttr[i]);
			}
		}

		void randomAccessWriteU2(int pos, int val) {
			int savePos = genPos;
			genPos = pos;
			writeU2(val);
			genPos = savePos;
		}

		int readU1() {
			return ((int) orig[origPos++]) & 0xFF;
		}

		int readU2() {
			int res = readU1();
			return (res << 8) + readU1();
		}

		int readU4() {
			int res = readU2();
			return (res << 16) + readU2();
		}

		void writeU1(int val) {
			gen[genPos++] = (byte) val;
		}

		void writeU2(int val) {
			writeU1(val >> 8);
			writeU1(val & 0xFF);
		}

		void writeU4(int val) {
			writeU2(val >> 16);
			writeU2(val & 0xFFFF);
		}

		void copy(int count) {
			for (int i = 0; i < count; ++i) {
				gen[genPos++] = orig[origPos++];
			}
		}

		byte[] readBytes(int count) {
			byte[] bytes = new byte[count];
			for (int i = 0; i < count; ++i) {
				bytes[i] = orig[origPos++];
			}
			return bytes;
		}

		void writeBytes(byte[] bytes) {
			for (int i = 0; i < bytes.length; ++i) {
				gen[genPos++] = bytes[i];
			}
		}

		int copyConstantPool(int constantPoolCount)
				throws UnsupportedEncodingException, IOException {
			int sdeIndex = -1;
			// copy const pool index zero not in class file
			for (int i = 1; i < constantPoolCount; ++i) {
				int tag = readU1();
				writeU1(tag);
				switch (tag) {
				case 7: // Class
				case 8: // String
					if (log.isDebugEnabled())
						log.debug(i + " copying 2 bytes");
					copy(2);
					break;
				case 9: // Field
				case 10: // Method
				case 11: // InterfaceMethod
				case 3: // Integer
				case 4: // Float
				case 12: // NameAndType
					if (log.isDebugEnabled())
						log.debug(i + " copying 4 bytes");
					copy(4);
					break;
				case 5: // Long
				case 6: // Double
					if (log.isDebugEnabled())
						log.debug(i + " copying 8 bytes");
					copy(8);
					i++;
					break;
				case 1: // Utf8
					int len = readU2();
					writeU2(len);
					byte[] utf8 = readBytes(len);
					String str = new String(utf8, "UTF-8");
					if (log.isDebugEnabled())
						log.debug(i + " read class attr -- '" + str + "'");
					if (str.equals(nameSDE)) {
						sdeIndex = i;
					}
					writeBytes(utf8);
					break;
				default:
					throw new IOException("unexpected tag: " + tag);
				}
			}
			return sdeIndex;
		}

		void writeUtf8ForSDE() {
			int len = nameSDE.length();
			writeU1(1); // Utf8 tag
			writeU2(len);
			for (int i = 0; i < len; ++i) {
				writeU1(nameSDE.charAt(i));
			}
		}
	}

	public static void evaluateNodes(Node.Nodes nodes, SmapStratum s,
			HashMap innerClassMap, boolean breakAtLF) {
		try {
			nodes.visit(new SmapGenVisitor(s, breakAtLF, innerClassMap));
		} catch (JasperException ex) {
		}
	}

	static class SmapGenVisitor extends Node.Visitor {

		private SmapStratum smap;
		private boolean breakAtLF;
		private HashMap innerClassMap;

		SmapGenVisitor(SmapStratum s, boolean breakAtLF, HashMap map) {
			this.smap = s;
			this.breakAtLF = breakAtLF;
			this.innerClassMap = map;
		}

		public void visitBody(Node n) throws JasperException {
			SmapStratum smapSave = smap;
			String innerClass = n.getInnerClassName();
			if (innerClass != null) {
				this.smap = (SmapStratum) innerClassMap.get(innerClass);
			}
			super.visitBody(n);
			smap = smapSave;
		}

		public void visit(Node.Declaration n) throws JasperException {
			doSmapText(n);
		}

		public void visit(Node.Expression n) throws JasperException {
			doSmapText(n);
		}

		public void visit(Node.Scriptlet n) throws JasperException {
			doSmapText(n);
		}

		public void visit(Node.IncludeAction n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.ForwardAction n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.GetProperty n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.SetProperty n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.UseBean n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.PlugIn n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.CustomTag n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.UninterpretedTag n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.JspElement n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.JspText n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.NamedAttribute n) throws JasperException {
			visitBody(n);
		}

		public void visit(Node.JspBody n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.InvokeAction n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.DoBodyAction n) throws JasperException {
			doSmap(n);
			visitBody(n);
		}

		public void visit(Node.ELExpression n) throws JasperException {
			doSmap(n);
		}

		public void visit(Node.TemplateText n) throws JasperException {
			Mark mark = n.getStart();
			if (mark == null) {
				return;
			}

			// Add the file information
			String fileName = mark.getFile();
			smap.addFile(unqualify(fileName), fileName);

			// Add a LineInfo that corresponds to the beginning of this node
			int iInputStartLine = mark.getLineNumber();
			int iOutputStartLine = n.getBeginJavaLine();
			int iOutputLineIncrement = breakAtLF ? 1 : 0;
			smap.addLineData(iInputStartLine, fileName, 1, iOutputStartLine,
					iOutputLineIncrement);

			// Output additional mappings in the text
			java.util.ArrayList extraSmap = n.getExtraSmap();

			if (extraSmap != null) {
				for (int i = 0; i < extraSmap.size(); i++) {
					iOutputStartLine += iOutputLineIncrement;
					smap
							.addLineData(iInputStartLine
									+ ((Integer) extraSmap.get(i)).intValue(),
									fileName, 1, iOutputStartLine,
									iOutputLineIncrement);
				}
			}
		}

		private void doSmap(Node n, int inLineCount, int outIncrement,
				int skippedLines) {
			Mark mark = n.getStart();
			if (mark == null) {
				return;
			}

			String unqualifiedName = unqualify(mark.getFile());
			smap.addFile(unqualifiedName, mark.getFile());
			smap.addLineData(mark.getLineNumber() + skippedLines, mark
					.getFile(), inLineCount - skippedLines, n
					.getBeginJavaLine()
					+ skippedLines, outIncrement);
		}

		private void doSmap(Node n) {
			doSmap(n, 1, n.getEndJavaLine() - n.getBeginJavaLine(), 0);
		}

		private void doSmapText(Node n) {
			String text = n.getText();
			int index = 0;
			int next = 0;
			int lineCount = 1;
			int skippedLines = 0;
			boolean slashStarSeen = false;
			boolean beginning = true;

			// Count lines inside text, but skipping comment lines at the
			// beginning of the text.
			while ((next = text.indexOf('\n', index)) > -1) {
				if (beginning) {
					String line = text.substring(index, next).trim();
					if (!slashStarSeen && line.startsWith("/*")) {
						slashStarSeen = true;
					}
					if (slashStarSeen) {
						skippedLines++;
						int endIndex = line.indexOf("*/");
						if (endIndex >= 0) {
							// End of /* */ comment
							slashStarSeen = false;
							if (endIndex < line.length() - 2) {
								// Some executable code after comment
								skippedLines--;
								beginning = false;
							}
						}
					} else if (line.length() == 0 || line.startsWith("//")) {
						skippedLines++;
					} else {
						beginning = false;
					}
				}
				lineCount++;
				index = next + 1;
			}

			doSmap(n, lineCount, 1, skippedLines);
		}
	}

	private static class PreScanVisitor extends Node.Visitor {

		HashMap map = new HashMap();

		public void doVisit(Node n) {
			String inner = n.getInnerClassName();
			if (inner != null && !map.containsKey(inner)) {
				map.put(inner, new SmapStratum("JSP"));
			}
		}

		HashMap getMap() {
			return map;
		}
	}

}
